package com.firemoss.atonement.entitymanager
{
	import com.firemoss.atonement.common.EventDispatcherWrapper;
	import com.firemoss.atonement.entitymanager.events.EntityPutEvent;
	import com.firemoss.atonement.entitymanager.mapper.EntityManagerMapper;
	import com.firemoss.atonement.mapper.Mapper;
	import com.firemoss.atonement.metadata.AtonementMetadata;
	import com.firemoss.atonement.metadata.DefaultMetadataBuilder;
	import com.firemoss.atonement.metadata.EntityMetadata;
	import com.firemoss.atonement.metadata.IMetadataBuilder;
	
	import flash.events.EventDispatcher;
	import flash.events.IEventDispatcher;
	import flash.utils.Dictionary;
	
	import mx.collections.ArrayCollection;
	import mx.collections.ICollectionView;
	import mx.collections.IList;
	
	import org.as3commons.lang.ClassUtils;
	import org.as3commons.reflect.Type;

	[Event( name="entityPut", type="com.firemoss.atonement.entitymanager.events.EntityPutEvent" )]
	public class EntityManager extends EventDispatcherWrapper implements IEntityManager, IEventDispatcher
	{

		//----------------------------------------------------------
		//   Static Properties 
		//----------------------------------------------------------

		// STATIC

		private static var managers : Dictionary = new Dictionary()

		//----------------------------------------------------------
		//   Static Functions 
		//----------------------------------------------------------

		public static function getEntityManager( name : String="" ) : EntityManager
		{
			return managers[ name ] as EntityManager
		}

		//----------------------------------------------------------
		// Public Properties 
		//----------------------------------------------------------

		public var mapper : EntityManagerMapper

		private var _metadataBuilder : IMetadataBuilder = new DefaultMetadataBuilder()

		public function get metadataBuilder() : IMetadataBuilder
		{
			return _metadataBuilder
		}

		public function set metadataBuilder( value : IMetadataBuilder ) : void
		{
			_metadataBuilder = value
		}

		// PUBLIC PROPERTIES

		private var _name : String

		public function get name() : String
		{
			return _name
		}

		public function set name( value : String ) : void
		{
			if ( managers[ _name ])
			{
				managers[ _name ] == null
			}

			_name = value

			if ( value != null )
			{
				managers[ value ] = this
			}
		}

		//----------------------------------------------------------
		// Private Properties 
		//----------------------------------------------------------

		// PRIVATE PROPERTIES

		private var entityCaches : Dictionary = new Dictionary()
		private var entityClassMap : Dictionary = new Dictionary()

		//----------------------------------------------------------
		//   Constructor 
		//----------------------------------------------------------

		// CONSTRUCTOR

		public function EntityManager()
		{
			mapper = new EntityManagerMapper( this )
			name = "";
		}

		//----------------------------------------------------------
		// Public Methods 
		//----------------------------------------------------------

		public function constrictCache( collection : ICollectionView, clazz : Class ) : void
		{
			var cache : Dictionary = getEntityCacheForClass( clazz )

			for each ( var entityContainer : IEntityContainer in cache )
			{
				if ( !collection.contains( entityContainer.entity ))
				{
					remove( entityContainer.entity )
				}
			}
		}

		public function get( clazz : Class, id : * ) : Object
		{
			var container : IEntityContainer = getEntityContainerByClassAndId( clazz, id )

			return container.entity
		}

		// TODO:  unit test
		public function getAll( clazz : Class ) : Array
		{
			var cache : Dictionary = this.getEntityCacheForClass( clazz )
			var ary : Array = []

			for each ( var cont : IEntityContainer in cache )
			{
				ary.push( cont.entity )
			}

			return ary
		}

		// TODO:  unit test
		public function getAllAsCollection( clazz : Class ) : ArrayCollection
		{
			return new ArrayCollection( getAll( clazz ))
		}

		public function getByName( className : String, id : * ) : Object
		{
			var clazz : Class = entityClassMap[ className ]

			return get( clazz, id )
		}
		
		public function getByExample( example : Object ) : Object
		{
			var id : * = getEntityIdentifier( example )
			var clazz = ClassUtils.forInstance( example )
			
			return get( clazz, id )
		}

		// PUBLIC METHODS

		public function getEntityMetadata( entity : Object ) : EntityMetadata
		{
			var md : AtonementMetadata = metadataBuilder.buildMetadata( entity )

			return md.entityMetadata
		}

		// TODO:  unit test
		/**
		 * Removes all cached instances of a given class
		 */
		public function purge( clazz : Class ) : void
		{
			entityCaches[ clazz ] = new Dictionary()
		}

		public function put( entity : Object ) : Object
		{
			var emd : EntityMetadata = getEntityMetadata( entity )
			var value : *
			var evt : EntityPutEvent = new EntityPutEvent( entity, false )
			var result : Object

			if ( emd )
			{
				// Store or update this entity
				var container : IEntityContainer = getEntityContainerByEntity( entity )

				if ( container.entity == null )
				{
					container.entity = entity

					// Look funny?  Yeah, it is.  It's how we force a crawl the graph: the entity may have a related entities that aren't references to themself
					result = mapper.map( entity, entity )

					evt.entityUpdated = false
				}
				else
				{
					result = mapper.map( entity, container.entity )

					evt.entityUpdated = true
				}

				dispatchEvent( evt )
			}
			else
			{
				throw new Error( "The EntityManager can't put() an instance of " + Type.forInstance( entity ).fullName + " because it's not an entity.  Mark an Id property, please." );
			}

			return result

		}
		
		public function putList( list : IList ) : void
		{
			var item : Object
			var existing : Object
			
			// If we're dealing with something like an ArrayCollection, we can do this really quickly
			if ( Object( list ).hasOwnProperty( "source" ) )
			{
				var newSource : Array = []
					
				for each ( item in list )
				{
					existing = getByExample( item )
					newSource.push( existing == null ? item : existing )
					// Will implicitly merge the detached item onto the existing instance
					put( item )
				}
				
				Object(list).source = newSource
			}
			// Otherwise, we'll need to go through the IList interface
			{
				for each ( item in list )
				{
					existing = getByExample( item )
					var idx : int = list.getItemIndex( item )
						
					// If it exists in the cache, replace the detached item with the existing instance
					if ( existing )
					{
						list.removeItemAt( idx )
						list.addItemAt( existing, idx )
					}
					
					// Will implicitly merge the detached item onto the existing instance
					put( item )
				}
			}
		}

		public function remove( entity : Object ) : void
		{
			var id : * = getEntityIdentifier( entity )
			var cache : Dictionary = getEntityCacheForInstance( entity )

			if ( cache[ id ] != null )
			{
				cache[ id ] = null
				delete cache[ id ]
			}
		}

		//----------------------------------------------------------
		// Private Methods 
		//----------------------------------------------------------

		// PRIVATE METHODS

		private function getEntityCacheForClass( clazz : Class ) : Dictionary
		{
			if ( !entityCaches[ clazz ])
			{
				entityCaches[ clazz ] = new Dictionary()

				var md : EntityMetadata = metadataBuilder.buildMetadataForClass( clazz ).entityMetadata
				entityClassMap[ md.entityName ] = clazz
			}

			return entityCaches[ clazz ]
		}

		private function getEntityCacheForInstance( instance : Object ) : Dictionary
		{
			return getEntityCacheForClass( ClassUtils.forInstance( instance ));
		}

		private function getEntityContainer( cache : Dictionary, id : * ) : IEntityContainer
		{
			if ( !cache[ id ])
			{
				// TODO:  factory out which type to create
				cache[ id ] = new StronglyReferencedEntityContainer()
			}

			return cache[ id ]
		}

		private function getEntityContainerByClassAndId( clazz : Class, id : * ) : IEntityContainer
		{
			var cache : Dictionary = getEntityCacheForClass( clazz )

			return getEntityContainer( cache, id )
		}

		private function getEntityContainerByEntity( entity : Object ) : IEntityContainer
		{
			var id : * = getEntityIdentifier( entity )
			var cache : Dictionary = getEntityCacheForInstance( entity )

			return getEntityContainer( cache, id )
		}

		private function getEntityIdentifier( entity : Object ) : *
		{
			var md : EntityMetadata = getEntityMetadata( entity )

			return entity[ md.idPropertyName ];
		}
	}
}
